<?php
/** ================================
 *  Intellect Board 3
 *  http://intblite.ru
 *  Модуль библиотеки типовых действий над профилем пользователя
 *  Примечание: модуль не содержит интерактивной части, она вынесена в user.php
 *
 *  ================================ */

class userlib extends Module {
  /** Осуществляет занесение данных пользователя в БД (предварительно проведя валидацию)
   *
   * @global Application $app
   * @param array $data Хеш с данными о пользователе. Должен содержать два подмассива: basic (базовая информация, которая пойдет в модуль auth) и settings -- информация о настройках профиля
   * @return array Массив ошибок при валидации пользователя (или пустой при успешной регистрации)
   */
  function register_user($data) {
    global $app;
    if (isset($data['basic']['id'])) unset($data['basic']['id']); // сбрасываем идентификатор, если он вдруг установлен
    if (!isset($data['basic']['password'])) $data['basic']['password']=substr(sha1(rand(),rand(7,9))); //если пароль не установлен, генерируем случайный
    $activate_mode=$app->get_opt('user_activation');
    $data['basic']['active']=($activate_mode==0) ? 1 : 0; // если активация пользователя требуется, то пользователь изначально неактивен

    $crypt_mode=$app->get_opt('user_pass_crypt'); // проверяем, какой режим шифрования задан в настройках форума. Если он равен -1, значит, пользователь может сам выбирать, шифровать ему пароль или нет
    if ($crypt_mode==-1) $data['basic']['pass_crypt']=isset($data['basic']['pass_crypt']) ? $data['basic']['pass_crypt'] : 0; // если пользователь ничего не выбрал, пароль не шифруем
    else $data['basic']['pass_crypt']=$crypt_mode; // если режим задан явно, шифруем в соответствии с этим режимом

    $errors = $this->validate_user($data['basic']);

    $reg_timeout=$app->get_opt('userlib_register_timeout');
    if ($reg_timeout && $app->load_module('antibot')) { // проверяем, что предыдущая регистрация с этого IP была не менее 15 секунд назад
      $antibot = new antibot;
      if ($antibot->timeout_check('userlib_regiser', $reg_timeout)) $errors[]=array('text'=>'Предыдущая регистрация с данного адреса была менее чем '.$reg_timeout.' секунд назад','level'=>3);
    }
    $result = false;
    if (empty($errors)) { // если при валидации пользователя не произошло ошибок
      $data['basic']['canonical']=$this->canonize_name($data['basic']['login']);
      $data['basic']['id']=$app->user->register_profile($data['basic']);
      if ($data['basic']['id']) { // если пользователя удалось зарегистрировать и ему дали идентификатор
        $result = true;
        $options = $app->get_opt_all('user',3); // получаем настройки системного пользователя NewUser, который всегда имеет номер 3
        if (!$options) $options=array();
        $options = array_merge($options,$data['options']); // TODO: уточнить вопрос с приоритетами, может быть, нужно наоборот
        if ($app->load_module('common')) {
          $common = new common;     
          $common->save_opt('user',$data['basic']['id'],$options);
        }
        if ($activate_mode==0) $app->user->do_login($data['basic']['login'],$data['basic']['password']); // если активации пользователя не требуется, входим на форум сразу
        elseif ($activate_mode==2) { // если включена активация пользователя администратором
          // TODO: отправка почты админам (подумать, как получать их список)
        }
        else { // если включена активация пользователя через его ящик EMail
          $mail = new Html('activate','user');
          $data['key'] = $app->user->gen_auth_key($data['id'],'user','activate');
          $mail->set_html($data);
          // TODO: добавить выставление ссылки на сайт и названия форума
          $app->mail(array('to'=>$data['email'],'subj'=>'Подтверждение регистрации на сайте','to_name'=>$data['login'],'text'=>$mail));
        }
        // TODO: обработка загрузки аватара пользователя
        // TODO: дополнительные действия при регистрации, например, посылка ЛС с приветствием
        $this->update_user_cache($data); // обновляем кеш данных пользователя
      }
    }
    else $app->message($errors); // иначе -- выводим сообщения об ошибке

    return $result;
  }

  /** Осуществляет занесение данных пользователя в БД (предварительно проведя валидацию)
   *
   * @global Application $app
   * @param array $data Хеш с данными о пользователе
   * @return array Массив ошибок при валидации пользователя (или пустой при успешной регистрации)
   */
  function update_user($data) {
    global $app;
    if ($app->user->get_uid()!=$data['basic']['id']) {
      $app->message('Нельзя редактировать чужой профиль!',3);
      //TODO: добавить проверку на наличие прав редактирования чужого профиля
      return false;
    }

    $olddata = $app->user->load_user($data['basic']['id']);
    $change_email=($olddata['email']!=$data['basic']['email']);
    $change_login=($olddata['login']!=$data['basic']['login']);

    // TODO: подумать, где сделать проверку старого пароля в том случае, если пользователь хочет менять логин или EMail: здесь или в user.php

    if ($change_email) {
      $activate_mode=$app->get_opt('user_activation');
      $data['basic']['active']=($activate_mode==1) ? 0 : 1; // если включен режим активации пользователем, то деактивируем пользователя до момента, когда он подтвердит свой ящик
    }

    if ($data['basic']['password']) { // если новый пароль задан (проверка ввода подтверждения пароля будет делаться в user.php)
      $crypt_mode=$app->get_opt('user_pass_crypt'); // проверяем, какой режим шифрования задан в настройках форума. Если он равен -1, значит, пользователь может сам выбирать, шифровать ему пароль или нет
      if ($crypt_mode==-1) $data['basic']['pass_crypt']=isset($data['basic']['pass_crypt']) ? $data['basic']['pass_crypt'] : 1; // если пользователь ничего не выбрал, пароль шифруем обычным способом
      else $data['basic']['pass_crypt']=$crypt_mode; // если режим задан явно, шифруем в соответствии с этим режимом
    }
    else { // если новый пароль пустой, сбрасываем поля password и pass_crypt, чтобы избежать изменения пароля
      unset($data['basic']['password']);
      unset($data['basic']['pass_crypt']);
    }

    $errors = $this->validate_user($data['basic']);
    $result = false;
    if (empty($errors)) { // если при валидации пользователя не произошло ошибок
      $data['basic']['canonical']=$this->canonize_name($data['basic']['login']);
      $result=$app->user->update_profile($data['basic']);
      if ($result) { // если изменения в профиль внесены успешно
        $options = $app->get_opt_all('user',$data['basic']['id']); // получаем старые настройки пользователя и заменяем их на новые
        $options = array_merge($options,$data['options']); // TODO: уточнить вопрос с приоритетами, может быть, нужно наоборот
        $app->save_opt('user',$data['basic']['id'],$options);
        if ($change_email && $activate_mode==1) { // если включена активация пользователя через его ящик EMail
          $mail = new Html('activate.htm','user');
          $data['key'] = $app->user->gen_auth_key($data['id'],'user','activate');
          $mail->set_html($data);
          // TODO: добавить выставление ссылки на сайт и названия форума
          $app->mail(array('to'=>$data['email'],'subj'=>'Подтверждение регистрации на сайте','to_name'=>$data['login'],'text'=>$mail));
        }
        // TODO: обработка загрузки аватара пользователя
        $this->update_user_cache($data); // обновляем кеш данных пользователя в таблице user_cache
        $app->user->reset_cache($data['basic']['id']); // сбрасываем кеш в сессии
      }
    }
    else $app->message($errors); // иначе -- выводим сообщения об ошибке

    return $result;
  }

  /** Проверка валидности данных для регистрации/изменения профиля пользователя.
   * Используемые настройки:
   *  @param int $user_name_mode -- допустимые символы в имени пользователя
   *        0 -- все, кроме точки с запятой (;), запятой (,), кавычек одинарных и двойных (",') и обратного апострофа (`)
   *        1 -- алфавитно-цифровые символы (латиница+кириллица),
   *        2 -- алфавитно-цифровые символы (только латиница),
   *        3 -- только буквы и пробелы (латиница+кириллица)
   *        4 -- правильный идентификатор с точки зрения программиста (латиница, вторая и след буквы могут быть цифрами или _).
   *        5 -- регулярное выражение
   *  @param string $user_name_regexp -- регулярное выражение для проверки на допустимость имени в режиме №4
   *  **/
  function validate_user($data) {
    global $app;
    $result = array();
    if (!isset($data['id'])) $data['id']=false;

    // определяем рег. выражение для проверки имени
    $name_mode=$app->get_opt('user_name_mode');
    if ($name_mode==0) $regexp='/^[^,;"\'`]+$/';
    elseif ($name_mode==1) $regexp='/^[\p{L}\d ]+/u';
    elseif ($name_mode==2) $regexp='/^[\p{L} ]+/u';
    elseif ($name_mode==3) $regexp='/^[\w ]+/u';
    elseif ($name_mode==4) $regexp='/^\w[\w\d]*/u';
    elseif ($name_mode==5) $regexp=$app->get_opt('user_name_regexp');
    if ($regexp=='') $regexp='/^[^,;"\'`]+$/';

    // проверяем имя пользователя
    if (!isset($data['login']) || $data['login']=='') {
      $result[]=array('text'=>'Имя пользователя не может быть пустым!','level'=>3);
    }
    elseif (!preg_match($regexp,$data['login'])) {
      if ($name_mode==0) $text='Имя пользователя не должно содержать точки с запятой (;), запятой (,), кавычек одинарных и двойных (",\') и обратного апострофа (`).';
      elseif ($name_mode==1) $text='Имя пользователя должно содержать только алфавитно-цифровые символы кириллицы или латиницы.';
      elseif ($name_mode==2) $text='Имя пользователя должно содержать только алфавитно-цифровые символы латиницы.';
      elseif ($name_mode==3) $text='Имя пользователя должно содержать только буквы и пробелы (кириллица и латиница).';
      elseif ($name_mode==4) $text='Имя пользователя должно быть правильным идентификатором (содержать только буквы и цифры и начинаться с буквы).';
      elseif ($name_mode==5) $text='Имя пользователя должно соответствовать регулярному выражению '.$regexp.'.';
      $result[]=array('text'=>$text,'level'=>3);
    }

    // приведение имени к "каноническому" виду и проверка на уникальность
    $canonical_login = $this->canonize_name($data['login']);
    $test_uid=$app->user->get_uid_by_name($canonical_login);
    if ($test_uid && $test_uid!=$data['id']) $result[]=array('text'=>'Пользователь с таким (или очень похожим) именем уже существует на форуме!','level'=>3);

    // проверка правильности Email
    if (!isset($data['email']) || $data['email']=='') {
      $result[]=array('text'=>'Адрес электронной почты не может быть пустым!','level'=>3);
    }
    elseif (!$app->valid_email($data['email'])) {
      $result[]=array('text'=>'Адрес электронной почты не является правильным!','level'=>3);
    }
    else {
      // TODO: доделать проверку на существование домена

      list($mailuser,$host)=explode('@',$data['email']);
      if ($app->check_list('user_banned_mail',strtolower($host))) {
        $result[]=array('text'=>'Регистрация на адреса электронной почты, находящиеся в домене '.$host.', запрещена администратором форума!','level'=>3);
      }
    }

    $test_uid = $app->user->get_uid_by_email($data['email']); // проверка EMail на уникальность
    if ($test_uid && $test_uid!=$data['id']) $result[]=array('text'=>'Пользователь с таким адресом Email уже существует на форуме!','level'=>3);

    return $result;
  }

  function delete_user($id) {
    global $app;
    if ($app->user->get_uid()==$id) $app->user->do_logout(); // если удаляем текущего пользователя, то его сначала разлогиниваем
    $sql = 'DELETE FROM '.DB_prefix.'user_cache WHERE uid='.intval($id);
    $app->db->query($sql);

    // TODO: доделать очистку таблицы settings, а так же подумать, что сделать с таблицей priv_msg
    return $app->user->delete_profile($id);
  }

  /** Обновление таблицы user_cache с кешем данных пользователя.
   *
   * @param array $data Массив с данными пользователя
   */
  function update_user_cache($data) {
    global $app;
    $sql = 'DELETE FROM '.DB_prefix.'user_cache WHERE uid='.intval($data['uid']); // удаляем старую запись из кеша, если она есть
    $app->db->query($sql);
    $cachedata['name']=$data['login'];
    // TODO: доделать сохранение других данных в кеш, подумать, как отбирать только отображаемые данные
    $serialized = serialize($cachedata);
    $sql = 'INSERT INTO '.DB_prefix.'user_cache (uid,count,data) VALUES ('.intval($data['uid']).',0,"'.$app->slashes($serialized).'")';
    $app->db->query($sql);
  }

  /** Приведение имени пользователя к "каноническому" виду (замена похожих по начертанию букв и цифры) в целях недопущения регистрации пользователей с похожими именами **/
  function canonize_name($name) {
    $name = str_replace(' ','',strtolower(str_replace("\t",'',$name)));
    $name = str_replace(array('Й','Ц','У','К','Е','Н','Г','Ш','Щ','З','Х','Ъ','Ф','Ы','В','А','П','Р','О','Л','Д','Ж','Э','Я','Ч','С','М','И','Т','Ь','Б','Ю'),
            array('й','ц','у','к','е','н','г','ш','щ','з','х','ъ','ф','ы','в','а','п','р','о','л','д','ж','э','я','ч','с','м','и','т','ь','б','ю'),$name);
    $name = str_replace(array('0','1','6','а','в','е','з','и','к','о','р','с','у','х','ь','i','н','п','м','т'),
            array('o','l','b','a','b','e','3','u','k','o','p','c','y','x','b','l','h','n','m','t'),$name);
    $name = str_replace("ю","lo",$name);
    $name = str_replace("ы","bl",$name);
    return $name;
  }

}
?>
